/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package com.weknowall.app_common.zip.extra;

import java.util.zip.CRC32;
import java.util.zip.ZipException;

/**
 * This is a class that has been made significantly smaller (deleted a bunch of methods) and originally
 * is from the Apache Ant Project (http://ant.apache.org), org.apache.tools.zip package.
 * All license and other documentation is intact.
 * 
 * Adds Unix file permission and UID/GID fields as well as symbolic
 * link handling.
 * 
 * <p>
 * This class uses the ASi extra field in the format:
 * </p>
 *
 * <pre>
 *         Value         Size            Description
 *         -----         ----            -----------
 * (Unix3) 0x756e        Short           tag for this extra block type
 *         TSize         Short           total data size for this block
 *         CRC           Long            CRC-32 of the remaining data
 *         Mode          Short           file permissions
 *         SizDev        Long            symlink'd size OR major/minor dev num
 *         UID           Short           user ID
 *         GID           Short           group ID
 *         (var.)        variable        symbolic link filename
 * </pre>
 *
 * <p>
 * taken from appnote.iz (Info-ZIP note, 981119) found at <a href="ftp://ftp.uu.net/pub/archiving/zip/doc/">ftp://ftp.uu.net/pub/archiving/zip/doc/</a>
 * </p>
 * 
 * 
 * <p>
 * Short is two bytes and Long is four bytes in big endian byte and word order, device numbers are currently not supported.
 * </p>
 * 
 * <p>
 * Since the documentation this class is based upon doesn't mention the character encoding of the file name at all, it is assumed that it uses the current platform's default
 * encoding.
 * </p>
 */
public class AsiExtraField implements ZipExtraField, Cloneable {

  /**
   * Bits used for permissions (and sticky bit)
   * 
   * @since 1.1
   */
  final int PERM_MASK = 07777;
  /**
   * Indicates symbolic links.
   * 
   * @since 1.1
   */
  final int LINK_FLAG = 0120000;
  /**
   * Indicates plain files.
   * 
   * @since 1.1
   */
  final int FILE_FLAG = 0100000;
  /**
   * Indicates directories.
   * 
   * @since 1.1
   */
  final int DIR_FLAG = 040000;

  // ----------------------------------------------------------
  // somewhat arbitrary choices that are quite common for shared
  // installations
  // -----------------------------------------------------------

  /**
   * Default permissions for symbolic links.
   * 
   * @since 1.1
   */
  final int DEFAULT_LINK_PERM = 0777;
  /**
   * Default permissions for directories.
   * 
   * @since 1.1
   */
  final int DEFAULT_DIR_PERM = 0755;
  /**
   * Default permissions for plain files.
   * 
   * @since 1.1
   */
  final int DEFAULT_FILE_PERM = 0644;

  private static final ZipShort HEADER_ID = new ZipShort(0x756E);
  private static final int WORD = 4;
  /**
   * Standard Unix stat(2) file mode.
   * 
   * @since 1.1
   */
  private int mode = 0;
  /**
   * User ID.
   * 
   * @since 1.1
   */
  private int uid = 0;
  /**
   * Group ID.
   * 
   * @since 1.1
   */
  private int gid = 0;
  /**
   * File this entry points to, if it is a symbolic link.
   * 
   * <p>
   * empty string - if entry is not a symbolic link.
   * </p>
   * 
   * @since 1.1
   */
  private String link = "";
  /**
   * Is this an entry for a directory?
   * 
   * @since 1.1
   */
  private boolean dirFlag = false;

  /**
   * Instance used to calculate checksums.
   * 
   * @since 1.1
   */
  private CRC32 crc = new CRC32();

  /** Constructor for AsiExtraField. */
  public AsiExtraField() {
  }

  /**
   * The Header-ID.
   * 
   * @return the value for the header id for this extrafield
   * @since 1.1
   */
  public ZipShort getHeaderId() {
    return HEADER_ID;
  }

  /**
   * Length of the extra field in the local file data - without
   * Header-ID or length specifier.
   * 
   * @return a <code>ZipShort</code> for the length of the data of this extra field
   * @since 1.1
   */
  public ZipShort getLocalFileDataLength() {
    return new ZipShort(WORD // CRC
        + 2 // Mode
        + WORD // SizDev
        + 2 // UID
        + 2 // GID
        + getLinkedFile().getBytes().length);
    // Uses default charset - see class Javadoc
  }

  /**
   * Delegate to local file data.
   * 
   * @return the centralDirectory length
   * @since 1.1
   */
  public ZipShort getCentralDirectoryLength() {
    return getLocalFileDataLength();
  }

  /**
   * The actual data to put into local file data - without Header-ID
   * or length specifier.
   * 
   * @return get the data
   * @since 1.1
   */
  public byte[] getLocalFileDataData() {
    // CRC will be added later
    byte[] data = new byte[getLocalFileDataLength().getValue() - WORD];
    System.arraycopy(ZipShort.getBytes(getMode()), 0, data, 0, 2);

    byte[] linkArray = getLinkedFile().getBytes(); // Uses default charset - see class Javadoc
    // CheckStyle:MagicNumber OFF
    System.arraycopy(ZipLong.getBytes(linkArray.length),
        0, data, 2, WORD);

    System.arraycopy(ZipShort.getBytes(getUserId()),
        0, data, 6, 2);
    System.arraycopy(ZipShort.getBytes(getGroupId()),
        0, data, 8, 2);

    System.arraycopy(linkArray, 0, data, 10, linkArray.length);
    // CheckStyle:MagicNumber ON

    crc.reset();
    crc.update(data);
    long checksum = crc.getValue();

    byte[] result = new byte[data.length + WORD];
    System.arraycopy(ZipLong.getBytes(checksum), 0, result, 0, WORD);
    System.arraycopy(data, 0, result, WORD, data.length);
    return result;
  }

  /**
   * Delegate to local file data.
   * 
   * @return the local file data
   * @since 1.1
   */
  public byte[] getCentralDirectoryData() {
    return getLocalFileDataData();
  }

  /**
   * Set the user id.
   * 
   * @param uid the user id
   * @since 1.1
   */
  public void setUserId(int uid) {
    this.uid = uid;
  }

  /**
   * Get the user id.
   * 
   * @return the user id
   * @since 1.1
   */
  public int getUserId() {
    return uid;
  }

  /**
   * Set the group id.
   * 
   * @param gid the group id
   * @since 1.1
   */
  public void setGroupId(int gid) {
    this.gid = gid;
  }

  /**
   * Get the group id.
   * 
   * @return the group id
   * @since 1.1
   */
  public int getGroupId() {
    return gid;
  }

  /**
   * Indicate that this entry is a symbolic link to the given filename.
   * 
   * @param name Name of the file this entry links to, empty String
   *          if it is not a symbolic link.
   * 
   * @since 1.1
   */
  public void setLinkedFile(String name) {
    link = name;
    mode = getMode(mode);
  }

  /**
   * Name of linked file
   * 
   * @return name of the file this entry links to if it is a
   *         symbolic link, the empty string otherwise.
   * 
   * @since 1.1
   */
  public String getLinkedFile() {
    return link;
  }

  /**
   * Is this entry a symbolic link?
   * 
   * @return true if this is a symbolic link
   * @since 1.1
   */
  public boolean isLink() {
    return getLinkedFile().length() != 0;
  }

  /**
   * File mode of this file.
   * 
   * @param mode the file mode
   * @since 1.1
   */
  public void setMode(int mode) {
    this.mode = getMode(mode);
  }

  /**
   * File mode of this file.
   * 
   * @return the file mode
   * @since 1.1
   */
  public int getMode() {
    return mode;
  }

  /**
   * Indicate whether this entry is a directory.
   * 
   * @param dirFlag if true, this entry is a directory
   * @since 1.1
   */
  public void setDirectory(boolean dirFlag) {
    this.dirFlag = dirFlag;
    mode = getMode(mode);
  }

  /**
   * Is this entry a directory?
   * 
   * @return true if this entry is a directory
   * @since 1.1
   */
  public boolean isDirectory() {
    return dirFlag && !isLink();
  }

  /**
   * Populate data from this array as if it was in local file data.
   * 
   * @param data an array of bytes
   * @param offset the start offset
   * @param length the number of bytes in the array from offset
   * @since 1.1
   * @throws ZipException on error
   */
  public void parseFromLocalFileData(byte[] data, int offset, int length)
      throws ZipException {

    long givenChecksum = ZipLong.getValue(data, offset);
    byte[] tmp = new byte[length - WORD];
    System.arraycopy(data, offset + WORD, tmp, 0, length - WORD);
    crc.reset();
    crc.update(tmp);
    long realChecksum = crc.getValue();
    if (givenChecksum != realChecksum) {
      throw new ZipException("bad CRC checksum "
          + Long.toHexString(givenChecksum)
          + " instead of "
          + Long.toHexString(realChecksum));
    }

    int newMode = ZipShort.getValue(tmp, 0);
    // CheckStyle:MagicNumber OFF
    byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)];
    uid = ZipShort.getValue(tmp, 6);
    gid = ZipShort.getValue(tmp, 8);

    if (linkArray.length == 0) {
      link = "";
    }
    else {
      System.arraycopy(tmp, 10, linkArray, 0, linkArray.length);
      link = new String(linkArray); // Uses default charset - see class Javadoc
    }
    // CheckStyle:MagicNumber ON
    setDirectory((newMode & DIR_FLAG) != 0);
    setMode(newMode);
  }

  /**
   * Get the file mode for given permissions with the correct file type.
   * 
   * @param mode the mode
   * @return the type with the mode
   * @since 1.1
   */
  protected int getMode(int mode) {
    int type = FILE_FLAG;
    if (isLink()) {
      type = LINK_FLAG;
    }
    else if (isDirectory()) {
      type = DIR_FLAG;
    }
    return type | (mode & PERM_MASK);
  }

  @Override
  public Object clone() {
    try {
      AsiExtraField cloned = (AsiExtraField) super.clone();
      cloned.crc = new CRC32();
      return cloned;
    }
    catch (CloneNotSupportedException cnfe) {
      // impossible
      throw new RuntimeException(cnfe);
    }
  }
}
